local desc = [[
# 跑得快玩法简介

一种由3人游玩的扑克牌游戏。将扑克牌去掉大小王和3个2、1个A后进行。

游戏以轮次为基准进行，首先所有玩家初始分数为0分。每轮的流程：

* 洗牌，所有人摸16张牌。
* 第一轮由随机的玩家负责首回合出牌，此后由得分最低者首先出牌。
  不断按照回合进行，直到有玩家出完所有牌为止。
* 回合的流程如下：
  - 首先由一名玩家首先出牌。可以出任意牌型的牌。
  - 下家必须出相同牌型的且大小比上家更大的牌。只要手中有可以出的就必须出，否则自动Pass。
  - 其他两名玩家都Pass之后，剩下一名获胜的玩家开始下回合出牌。
  - 若下家只剩一张牌，则本回合出牌者出单牌时只能从点数最大的开始出。
* 有玩家出完所有牌之后，其获得本轮胜利，本轮结束，此时胜者按照其他玩家手中剩余牌计分：
  - 若其手中有16张牌，则从他那里获得16*400分
  - 若其手中有8~15张牌，则从他那里获得n*200分
  - 若其手中有2~7张牌，则从他那里获得n*100分
  - 若其只剩一张牌，则不计分

若某回合以炸弹结束，则胜者从其他两名玩家处各获得1000分。

每轮结束时，若有玩家分数达到20000分，则游戏结束。

## 允许的牌型

基本的点数计算（从小到大）：3~K，A，2

- 单牌：出一张牌
- 对子：两张点数相同的牌
- 连对：两对或两对以上相连的对子，如：33 44，88 99
- 顺子：五张或以上相连的单牌，如34567；最多顺到A，2不能进顺子
- 三带一、三带一对：三张点数相同的牌，配合任意单牌（或对子）
- 飞机：两组或以上点数相连的三张，配合相同数量的单牌或者对子
- 炸弹：四张点数相同的牌
]]



---返回牌型字符串
---@param cards integer[] @ 点数数组
---@return string|nil @ 返回牌型字符串
local function Cards_Pattern(cards)
  -- 卡牌排序（从小到大）
  local function sortCards(cards)
    local sorted = table.simpleClone(cards)
    table.sort(sorted)
    return sorted
  end

  -- 统计每张牌的出现次数及去重牌值列表
  local function countCardFrequency(sortedCards)
    local freq = {}      -- 键：牌值，值：出现次数
    local valueList = {} -- 按顺序排列的去重牌值
    local valueSet = {}  -- 去重集合

    for _, card in ipairs(sortedCards) do
      freq[card] = (freq[card] or 0) + 1
      if not valueSet[card] then
        valueSet[card] = true
        table.insert(valueList, card)
      end
    end
    return freq, valueList
  end

  -- 牌型判断核心函数（支持3张单独出牌）
  local function judgeCardType(cards)
    local sorted = sortCards(cards)
    local len = #sorted
    local freq, values = countCardFrequency(sorted)

    -- 1. 单牌（1张）
    if len == 1 then
      return sorted[1] .. "_one"
    end

    -- 2. 对子（2张相同，排除2的对子）
    if len == 2 and #values == 1 then
      local val = values[1]
      if val == 2 then
        return table.concat(sorted, ",") .. "_invalid" -- 2的对子无效
      end
      if freq[val] == 2 then
        return val .. "_pair"
      end
    end

    -- 3. 3张相同（可单独出牌，非2）
    if len == 3 and #values == 1 then
      local val = values[1]
      if val == 2 then
        return table.concat(sorted, ",") .. "_invalid" -- 3张2无效
      end
      if freq[val] == 3 then
        return val .. "_three" -- 3张单独出牌（如333、AAA）
      end
    end

    -- 4. 炸弹（4张相同，非2）
    if len == 4 and #values == 1 then
      local val = values[1]
      if val == 2 then
        return table.concat(sorted, ",") .. "_invalid" -- 2的炸弹无效
      end
      if freq[val] == 4 then
        return val .. "_bomb"
      end
    end

    -- 5. 顺子（5张及以上连续单牌，不含2）
    if len >= 5 and #values == len then
      local has2 = false
      for _, val in ipairs(values) do
        if val == 2 then
          has2 = true
          break
        end
      end
      if not has2 then
        local isContinuous = true
        for i = 2, len do
          if sorted[i] ~= sorted[i - 1] + 1 then
            isContinuous = false
            break
          end
        end
        if isContinuous then
          return table.concat(values, ",") .. "_straight"
        end
      end
    end

    -- 6. 连对（最少2个连对，非2的对子，连续递增）
    if len >= 4 and len % 2 == 0 then
      local pairCount = len / 2 -- 对子数量（n≥2）
      if pairCount < 2 then return end

      local allValidPairs = true
      for _, val in ipairs(values) do
        if freq[val] ~= 2 or val == 2 then
          allValidPairs = false
          break
        end
      end
      if not allValidPairs then return end

      if #values ~= pairCount then return end

      local isContinuous = true
      for i = 2, #values do
        if values[i] ~= values[i - 1] + 1 then
          isContinuous = false
          break
        end
      end
      if isContinuous then
        return table.concat(values, ",") .. "_pair_chain"
      end
    end

    -- 7. 3带1（3张非2 + 1张单牌）
    if len == 4 and #values == 2 then
      local threeVal, singleVal
      for _, val in ipairs(values) do
        if freq[val] == 3 and val ~= 2 then
          threeVal = val
        end
        if freq[val] == 1 then
          singleVal = val
        end
      end
      if threeVal and singleVal then
        return threeVal .. "_three_with_" .. singleVal
      end
    end

    -- 8. 3带1对（3张非2 + 1对非2）
    if len == 5 and #values == 2 then
      local threeVal, pairVal
      for _, val in ipairs(values) do
        if freq[val] == 3 and val ~= 2 then
          threeVal = val
        end
        if freq[val] == 2 and val ~= 2 then
          pairVal = val
        end
      end
      if threeVal and pairVal then
        return threeVal .. "_three_with_" .. pairVal .. "_pair"
      end
    end

    -- 9. 飞机（2组及以上连续3张，带单牌/对子，3张非2）
    local groupCount, isWithSingle
    if len % 4 == 0 then
      groupCount = len / 4 -- 3n + n（n≥2）
      isWithSingle = true
    elseif len % 5 == 0 then
      groupCount = len / 5 -- 3n + 2n（n≥2）
      isWithSingle = false
    end
    if groupCount and groupCount >= 2 then
      local threeValues = {}
      for _, val in ipairs(values) do
        if freq[val] == 3 and val ~= 2 then
          table.insert(threeValues, val)
        end
      end
      if #threeValues ~= groupCount then return end

      table.sort(threeValues)
      local isContinuous = true
      for i = 2, #threeValues do
        if threeValues[i] ~= threeValues[i - 1] + 1 then
          isContinuous = false
          break
        end
      end
      if not isContinuous then return end

      local attachValues = {}
      for _, val in ipairs(values) do
        if freq[val] ~= 3 then
          if isWithSingle then
            table.insert(attachValues, val)
          else
            if freq[val] == 2 and val ~= 2 then
              table.insert(attachValues, val)
            else
              attachValues = nil
              break
            end
          end
        end
      end
      if attachValues then
        local attachStr = table.concat(attachValues, ",")
        if isWithSingle then
          return table.concat(threeValues, ",") .. "_plane_with_" .. attachStr
        else
          return table.concat(threeValues, ",") .. "_plane_with_" .. attachStr .. "_pairs"
        end
      end
    end

    -- 无效牌型
    return table.concat(sorted, ",") .. "_invalid"
  end
  return judgeCardType(cards)
end


---比较两个牌型字符串的大小
---@param cards1 string|nil @ 第一个牌的牌型字符串
---@param cards2 string|nil @ 第二个牌的牌型字符串
---@return boolean @ 返回第一个牌型是否大于第二个牌型
local function Compare(cards1, cards2)
  if cards1 == nil or cards2 == nil then
    return false
  end
  -- 牌型优先级（数字越大优先级越高，不同类型按此比较）
  local typePriority = {
    invalid = 0,
    one = 1,              -- 单牌
    pair = 2,             -- 对子
    three = 3,            -- 3张单独
    three_with = 4,       -- 3带1
    three_with_pair = 5,  -- 3带1对
    pair_chain = 6,       -- 连对
    straight = 7,         -- 顺子
    plane_with = 8,       -- 飞机带单牌
    plane_with_pairs = 9, -- 飞机带对子
    bomb = 10             -- 炸弹
  }
  -- 辅助函数：解析牌型字符串，提取类型和关键值
  local function parseCardType(cardStr)
    -- 提取类型（最后一个下划线后的部分）
    local typeStart = string.find(cardStr, "_[^_]+$")
    local type = string.sub(cardStr, typeStart + 1)

    -- 提取数值部分（类型前的内容）
    local valueStr = string.sub(cardStr, 1, typeStart - 1)

    -- 解析数值（可能是单个值、范围或数组）
    local values = {}
    for val in string.gmatch(valueStr, "%d+") do
      table.insert(values, tonumber(val))
    end

    return {
      type = type,     -- 牌型（如"one"、"pair_chain"）
      values = values, -- 关键数值列表
      raw = cardStr    -- 原始字符串
    }
  end

  -- 比较两个牌型的大小
  -- 返回值：(isGreater, isLess)，分别表示"前者是否大于后者"、"前者是否小于后者"
  local function compareCards(card1, card2)
    -- 解析两个牌型
    local c1 = parseCardType(card1)
    local c2 = parseCardType(card2)

    -- 1. 处理无效牌型
    if c1.type == "invalid" and c2.type == "invalid" then
      return false, false -- 都无效，无法比较
    elseif c1.type == "invalid" then
      return false, true  -- 前者无效，后者有效 → 前者小
    elseif c2.type == "invalid" then
      return true, false  -- 后者无效，前者有效 → 前者大
    end

    -- 2. 严格判断是否为同一类型（类型字符串必须完全相同）
    local isSameType = (c1.type == c2.type)
    if not isSameType then
      -- 不同类型：按优先级比较
      local p1 = typePriority[c1.type]
      local p2 = typePriority[c2.type]
      return p1 > p2, p1 < p2
    end

    -- 3. 同类型比较（只有类型完全相同才进入这里）
    local type = c1.type
    if type == "one" or type == "pair" or type == "three" or type == "bomb" then
      -- 单牌/对子/3张/炸弹：直接比较基础牌值
      return c1.values[1] > c2.values[1], c1.values[1] < c2.values[1]
    elseif type == "pair_chain" then
      -- 连对：必须长度相同才能比较，否则无法比较（返回false,false）
      if #c1.values ~= #c2.values then
        return false, false -- 连对组数不同，无法比较
      end
      return c1.values[1] > c2.values[1], c1.values[1] < c2.values[1]
    elseif type == "straight" then
      -- 顺子：必须长度相同才能比较
      if #c1.values ~= #c2.values then
        return false, false -- 顺子长度不同，无法比较
      end
      return c1.values[1] > c2.values[1], c1.values[1] < c2.values[1]
    elseif type == "three_with" then
      -- 3带1：比较3张主牌的牌值（忽略带的单牌）
      return c1.values[1] > c2.values[1], c1.values[1] < c2.values[1]
    elseif type == "three_with_pair" then
      -- 3带1对：比较3张主牌的牌值（忽略带的对子）
      return c1.values[1] > c2.values[1], c1.values[1] < c2.values[1]
    elseif type == "plane_with" or type == "plane_with_pairs" then
      -- 飞机：必须组数相同才能比较（3张牌的数量相同）
      local groupCount1 = math.floor(#c1.values / 2) + 1 -- 计算飞机组数
      local groupCount2 = math.floor(#c2.values / 2) + 1
      if groupCount1 ~= groupCount2 then
        return false, false -- 飞机组数不同，无法比较
      end
      return c1.values[1] > c2.values[1], c1.values[1] < c2.values[1]
    end

    -- 未匹配的类型
    return false, false
  end
  local cards1_boolean, cards2_boolean = compareCards(cards1, cards2)
  return cards1_boolean == true
end

---规则筛选
---@param room Room
---@param card_ids integer[] @ 已选择的牌
---@return boolean @ 这些牌可以出吗？
local function getPattern(room, card_ids)
  local nums = table.map(card_ids, function(cid, index, array)
    if Fk:getCardById(cid).number == 1 then
      return 14
    elseif Fk:getCardById(cid).number == 2 then
      return 15
    else
      return Fk:getCardById(cid).number
    end
  end)
  local n = room:getBanner("@pattern")
  if n == nil then
    local pattern = Cards_Pattern(nums)
    if pattern == nil or string.find(pattern, "invalid") then return false end
    room:setBanner("@pattern", pattern)
    return true
  else
    local pattern = room:getBanner("@pattern")
    local num_pattern = Cards_Pattern(nums)
    if num_pattern == nil or string.find(num_pattern, "invalid") then return false end
    return Compare(num_pattern, pattern)
  end
end


---每次的询问
---@param room Room
---@param winner ServerPlayer @当前出牌者(已出牌)
---@param card_ids2 integer[] @ 当期出牌者出的牌
---@return ServerPlayer @ 本次询问出牌胜利者
local phase = function(room, winner, card_ids2)
  local player_now = winner.next
  while player_now ~= winner do
    local card_ids1
    for i = 1, 10, 1 do
      card_ids1 = room:askToCards(player_now, {
        max_num = player_now:getHandcardNum(),
        min_num = 1,
        pattern = ".|.|.|hand",
        prompt = "#runfast-play",
        cancelable = true,
        skill_name = "runfast-play",
      })
      if #card_ids1 ~= 0 and getPattern(room, card_ids1) and Compare(Cards_Pattern(card_ids1), Cards_Pattern(card_ids2)) then
        room:moveCardTo(card_ids1, Card.Processing)
        if player_now:getHandcardNum() == 0 then
          room:moveCardTo(card_ids1, Card.DiscardPile)
          return player_now
        end
        winner = player_now
        player_now = winner.next
        break
      elseif #card_ids1 == 0 then
        player_now = player_now.next
        break
      else
        room:doBroadcastNotify("ShowToast", "出牌不合规，请重新出牌，还剩" .. (10 - i) .. "次出牌机会")
      end
    end
  end
  if #room:getCardsByArea(Card.Processing) > 0 then
    room:moveCardTo(room:getCardsByArea(Card.Processing), Card.DiscardPile)
  end
  return winner
end



---回合
---@param room Room
---@param winner ServerPlayer
local turn = function(room, winner)
  local players = {}
  local player = winner
  room:setBanner("@pattern", nil)
  while #players ~= 3 do
    table.insertIfNeed(players, player)
    player = player.next
  end
  local card_ids
  for i = 1, 10, 1 do
    card_ids = room:askToCards(winner, {
      max_num = winner:getHandcardNum(),
      min_num = 1,
      pattern = ".|.|.|hand",
      prompt = "#runfast-play",
      cancelable = true,
      skill_name = "runfast-play",
    })
    if #card_ids ~= 0 and getPattern(room, card_ids) == true then
      room:moveCardTo(card_ids, Card.Processing)
      if winner:getHandcardNum() == 0 then
        room:moveCardTo(card_ids, Card.DiscardPile)
        return
      end
      break
    elseif #card_ids == 0 then
      room:setBanner("now_turn", winner.next)
      return
    elseif #card_ids ~= 0 and getPattern(room, card_ids) == false then
      room:doBroadcastNotify("ShowToast", "出牌不合规，请重新出牌，还剩" .. (10 - i) .. "次出牌机会")
    end
  end

  while true do
    local winner_phase = phase(room, winner, card_ids)
    if winner_phase then
      room:setBanner("now_turn", winner_phase)
      break
    end
  end
end

---轮次
---@param room Room
local round = function(room)
  while true do
    local winner = room:getBanner("now_turn")
    if winner == nil then
      room:setBanner("now_turn", table.find(room.players, function(p) return p.seat == 1 end))
      winner = room:getBanner("now_turn")
    end
    turn(room, winner)
    if table.find(room.players, function(p) return p:getHandcardNum() == 0 end) then
      return table.find(room.players, function(p) return p:getHandcardNum() == 0 end)
    end
  end
end




---给分
---@param winner ServerPlayer
---@param player ServerPlayer
---@param room Room
local function giveScore(winner, player, room)
  local handnum = player:getHandcardNum()
  if handnum == 16 then
    room:setPlayerMark(winner, "@score", winner:getMark("@score") + 16 * 400)
    room:setPlayerMark(player, "@score", player:getMark("@score") - 16 * 400)
  elseif handnum >= 8 and handnum <= 15 then
    room:setPlayerMark(winner, "@score", winner:getMark("@score") + handnum * 200)
    room:setPlayerMark(player, "@score", player:getMark("@score") - handnum * 200)
  elseif handnum >= 2 and handnum <= 7 then
    room:setPlayerMark(winner, "@score", winner:getMark("@score") + handnum * 100)
    room:setPlayerMark(player, "@score", player:getMark("@score") - handnum * 100)
  end
end


--- 让玩家摸牌
--- @param room Room @ 房间
---@param player ServerPlayer @ 摸牌的玩家
---@param num integer @ 摸牌数
---@param skillName? string @ 技能名
---@param fromPlace? DrawPilePos @ 摸牌的位置，默认牌堆顶
---@param moveMark? table|string @ 移动后自动赋予标记，格式：{标记名(支持-inarea后缀，移出值代表区域后清除), 值}
---@return integer[] @ 摸到的牌
local drawCards = function(room, player, num, skillName, fromPlace, moveMark)
  if num < 1 then
    return {}
  end

  local drawData = DrawData:new {
    who = player,
    num = num,
    skillName = skillName,
    fromPlace = fromPlace or "top",
  }

  num = drawData.num
  fromPlace = drawData.fromPlace
  player = drawData.who

  local topCards = room:getNCards(num, fromPlace)
  table.sort(topCards, function(a, b)
    local a_num = Fk:getCardById(a).number
    local b_num = Fk:getCardById(b).number
    if a_num == 1 then
      a_num = 14
    end
    if a_num == 2 then
      a_num = 15
    end
    if b_num == 1 then
      b_num = 14
    end
    if b_num == 2 then
      b_num = 15
    end
    return a_num > b_num
  end)
  room:moveCards({
    ids = topCards,
    to = player,
    toArea = Card.PlayerHand,
    moveReason = fk.ReasonDraw,
    proposer = player,
    skillName = skillName,
    moveMark = moveMark,
  })

  return { table.unpack(topCards) }
end

---游戏环节
---@param room Room
local game = function(room)
  while true do
    local winner = round(room)
    if winner then
      local winner_score = winner:getMark("@score")
      local players = { winner.next, winner.next.next }
      for _, p in ipairs(players) do
        giveScore(winner, p, room)
      end
      if table.find(room.players, function(p) return p:getMark("@score") >= 20000 end) then
        room:gameOver("lord")
        return
      end
      for _, p in ipairs(room.players) do
        room:throwCard(p:getCardIds("h"), nil, p, nil)
      end
      room:shuffleDrawPile()
      for _, p in ipairs(room.players) do
        drawCards(room, p, 16)
      end
    end
  end
end


-- 决定初始牌堆以及初始游戏外区域的函数
-- 需要返回两个数组，一个是牌堆，一个是游戏外（void）
local function buildDrawPile()
  local allCardIds = Fk:getAllCardIds()
  local void = {}
  local drawCards = table.simpleClone(allCardIds)
  for i = #allCardIds, 1, -1 do
    local id = allCardIds[i]
    if Fk:getCardById(id).package.name ~= "poker-deck" then
      table.removeOne(drawCards, id)
    else
      if Fk:getCardById(id).number == 2 and Fk:getCardById(id).suit ~= Card.Spade then
        table.removeOne(drawCards, id)
      elseif Fk:getCardById(id).number == 1 and Fk:getCardById(id).suit == Card.Spade then
        table.removeOne(drawCards, id)
      end
    end
  end
  return drawCards, void
end


--- 准备房间牌堆
---@param seed integer @ 随机种子
local function prepareDrawPile(room, seed)
  local gamemode = Fk.game_modes[room.settings.gameMode]
  assert(gamemode)

  local draw_pile, void_pile = buildDrawPile()

  table.shuffle(draw_pile, seed)

  room.draw_pile = draw_pile
  for _, id in ipairs(room.draw_pile) do
    room:setCardArea(id, Card.DrawPile, nil)
  end

  room.void = void_pile
  for _, id in ipairs(room.void) do
    room:setCardArea(id, Card.Void, nil)
  end
end



local logic = function()
  ---@class DescLogic: GameLogic
  local desclogic = GameLogic:subclass("DescLogic")

  --- 安排座位
  function GameLogic:adjustSeats()
    local player_circle = {}
    local players = self.room.players
    local p = 1
    table.shuffle(players)
    p = math.random(1, #players)
    for j = p, #players do
      table.insert(player_circle, players[j])
    end
    for j = 1, p - 1 do
      table.insert(player_circle, players[j])
    end

    self.room:arrangeSeats(player_circle)
  end

  function desclogic:chooseGenerals()
    local room = self.room
    for _, p in ipairs(room.players) do
      local avatar = p._splayer:getAvatar()
      if avatar == "anjiang" then avatar = table.random { "blank_shibing", "blank_nvshibing" } end
      local avatar_general = Fk.generals[avatar] or Fk.generals["sunce"] or Fk.generals["diaochan"]
      room:setPlayerProperty(p, "general", avatar_general.name)
      room:setPlayerProperty(p, "kingdom", avatar_general.kingdom)
      room:setPlayerProperty(p, "role_shown", true)
    end
  end

  function desclogic:prepareDrawPile()
    local room = self.room
    local seed = math.random(2 << 32 - 1)
    prepareDrawPile(room, seed)
    room:doBroadcastNotify("PrepareDrawPile", seed)
    room:doBroadcastNotify("UpdateDrawPile", #room.draw_pile)
  end

  function desclogic:broadcastGeneral() end

  function desclogic:attachSkillToPlayers() end

  function desclogic:prepareForStart() end

  function desclogic:action()
    for _, p in ipairs(self.room.players) do
      drawCards(self.room, p, 16)
      self.room:setPlayerMark(p, "@score", 0)
    end
    game(self.room)
  end

  return desclogic
end




local mode = fk.CreateGameMode {
  name = "runfast_game",
  minPlayer = 3,
  maxPlayer = 3,
  logic = logic,
  whitelist = function(self, pkg)
    if pkg.type ~= Package.CardPack then return true end
    return pkg.name == "poker-deck"
  end,
}

Fk:loadTranslationTable {
  ["runfast_game"] = "跑得快",
  ["@score"] = "分数",
  [":runfast_game"] = desc,
  ["now_turn"] = "当前回合角色",
  ["runfast-play"] = "跑得快",
  ["#runfast-play"] = "请出牌。",
  ["@pattern"] = "出牌类型"
}

return mode
